# coding: utf-8
'''
Script to convert ePub book descriptors to indicate the book's language.
Needed for some books to work with iBooks built-in dictionary.

Creates a backup of each file before editing. See USAGE variable below or run with -h option.

Author: Łukasz Jancewicz
http://www.jancewicz.net/

Distributed under the GNU General Public License Version 3:
http://www.gnu.org/licenses/gpl.html
'''
import glob
import logging
import shutil
import optparse
import os.path
from xml.dom import minidom
import zipfile


USAGE = r"""
%prog [options] file1 [file2 ...]

Examples:
    %prog my_book.epub
    %prog first.epub second.epub third.epub
    %prog mydir\*.epub
"""


logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(message)s')


class Error(Exception):
    '''For all kinds of exceptions in this module.'''
    def show(self):
        logging.error("ERROR! %s", self)


class Fixer(object):
    '''
    Changes language in given files with given LanguageSetter.
    '''
    CONTENT_FILENAME = 'content.opf'

    @classmethod
    def fix_files(cls, filenames, language):
        lang_setter = LanguageSetter(language)
        for filename in filenames:
            try:
                Fixer(filename, lang_setter).fix()
            except Error, exc:
                exc.show()

    def __init__(self, filename, lang_setter):
        self._filename = filename
        self._backup_filename = self._get_backup_filename()
        self._lang_setter = lang_setter

    def fix(self):
        logging.info('Processing file "%s" ...', self._filename)
        self._backup_original_file()
        self._set_language_in_zip()

    def _get_backup_filename(self):
        dirname, filename = os.path.split(os.path.abspath(self._filename))
        backup_filename = '%s.bak' % filename
        return os.path.join(dirname, backup_filename)

    def _backup_original_file(self):
        shutil.copyfile(self._filename, self._backup_filename)

    def _set_language_in_zip(self):
        content = self._extract_content_from_zip()
        modified = self._lang_setter.modify(content)
        self._save_content_in_zip(modified)

    def _extract_content_from_zip(self):
        try:
            archive = zipfile.ZipFile(self._filename, 'r')
        except zipfile.BadZipfile, exc:
            raise Error("Error while opening archive %s: %s" % (self._filename, exc))
        try:
            return archive.read(self.CONTENT_FILENAME)
        finally:
            archive.close()

    def _save_content_in_zip(self, content):
        source = zipfile.ZipFile(self._backup_filename, 'r')
        try:
            target = zipfile.ZipFile(self._filename, 'w', zipfile.ZIP_DEFLATED)
            try:
                self._copy_with_content_modified(source, target, content)
            finally:
                target.close()
        finally:
            source.close()

    def _copy_with_content_modified(self, source, target, content):
        for item in source.infolist():
            if item.filename == self.CONTENT_FILENAME:
                target.writestr(item, content)
            else:
                filestr = source.read(item.filename)
                target.writestr(item, filestr)


class LanguageSetter(object):
    '''
    Updates given XML string by changing its dc:language property to given language.
    '''
    def __init__(self, language):
        self._language = language

    def modify(self, content):
        dom = minidom.parseString(content)
        try:
            self._set_language_in_dom(dom)
            return dom.toxml(encoding='utf-8')
        finally:
            dom.unlink()

    def _set_language_in_dom(self, dom):
        language_node = self._find_language_node(dom)
        old_text = language_node.firstChild
        logging.info('Replacing language "%s" with "%s".', old_text.data, self._language)
        new_text = minidom.Text()
        new_text.data = self._language
        language_node.replaceChild(new_text, old_text)

    @staticmethod
    def _find_single_node(parent, name):
        results = parent.getElementsByTagName(name)
        if len(results) > 1:
            raise Error('More than one "%s" node found.' % name)
        return results[0]

    def _find_language_node(self, dom):
        package = self._find_single_node(dom, 'package')
        metadata = self._find_single_node(package, 'metadata')
        return self._find_single_node(metadata, 'dc:language')


class OptionReader(object):
    '''
    Parses command-line options and provides results on "language" and "filenames" variables.
    '''
    def __init__(self, parser_class=optparse.OptionParser, argv=None):
        self._argv = argv
        self._parser = self._get_option_parser(parser_class)
        self.language = None
        self.filenames = []

    @staticmethod
    def _get_option_parser(parser_class):
        parser = parser_class(usage=USAGE)
        parser.add_option('-l', '--lang', dest='language',
                          help='language to change the file to', default='en')
        return parser

    def parse(self):
        options, filemasks = self._parser.parse_args(self._argv)
        if not filemasks:
            self._parser.error("At least one file needed.")
        self.language = options.language
        self.filenames = self._extract_filenames(filemasks)

    def _extract_filenames(self, filemasks):
        result = []
        for mask in filemasks:
            result.extend(self._get_filenames_from_mask(mask))
        return result

    def _get_filenames_from_mask(self, mask):
        files = glob.glob(mask)
        if not files:
            self._parser.error("File not found: %s" % mask)
        return files


def run_from_commandline():
    rd = OptionReader()
    rd.parse()
    Fixer.fix_files(rd.filenames, rd.language)
    logging.info('Done.')


if __name__ == '__main__':
    run_from_commandline()
